Chapter 9

Theming, Styling, and Custom Widgets

Session 9

Learning Objectives

By the end of this chapter, you will be able to:

1

Why Theming Matters

Consistent theming is fundamental to creating professional, maintainable Flutter applications.

Benefits of Theming

  • Coherent visual identity: Consistent theming gives your app a coherent visual identity and simplifies maintenance.
  • Centralized styles: Centralized styles prevent duplicated styling code and make global changes straightforward.
  • Accessibility support: Theming supports accessibility (contrasting colors, scalable text) and reduces visual bugs.
2

App-Level Theming with ThemeData

Flutter's ThemeData provides a comprehensive system for app-wide styling.

Setting Up Theming

Wrap your app with MaterialApp(theme: ThemeData(...)) and provide darkTheme and themeMode for light/dark switching.

Common ThemeData Fields

primaryColor, colorScheme, textTheme, iconTheme, buttonTheme, inputDecorationTheme, appBarTheme, floatingActionButtonTheme.

Example Pattern (Conceptual)

MaterialApp(
  title: 'Afrilen App',
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
    textTheme: TextTheme(
      headline6: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
      bodyText2: TextStyle(fontSize: 14),
    ),
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ElevatedButton.styleFrom(shape: StadiumBorder()),
    ),
  ),
  darkTheme: ThemeData.dark(),
  themeMode: ThemeMode.system,
  home: HomePage(),
)

Guideline: Use ColorScheme and fromSeed or ColorScheme.light/dark to keep colors harmonious.

3

Text Styles and Typography

Consistent typography is key to professional app design.

TextTheme Usage

  • Define a TextTheme in ThemeData and reference it via Theme.of(context).textTheme.
  • Prefer semantic styles (headline, subtitle, body) instead of hard-coded sizes at use sites.

Usage Example

Text('Welcome', style: Theme.of(context).textTheme.headline6);

Best practice: Create a typography spec (font families, sizes, weights) and export as a centralized file for consistency.

4

Color Palettes and Constants

Organizing colors systematically makes theming manageable and consistent.

Color Token Organization

  • Keep color tokens in one place (e.g., app_colors.dart) with semantic names: primary, accent, background, surface, error, textPrimary, textSecondary.
  • Use semantic names at the widget level, not raw hex values.

Example Token File (Conceptual)

class AppColors {
  static const primary = Color(0xFF0066CC);
  static const accent = Color(0xFFFFC107);
  static const background = Color(0xFFF6F7FB);
}
5

Button, Input, and Card Theming

Theming common components ensures consistency across your app.

Component Theming

  • Override ElevatedButtonThemeData, InputDecorationTheme, and CardTheme in ThemeData so components look consistent across the app.
  • Avoid styling buttons at every call site; style centrally and pass minimal overrides when needed.

Example: InputDecorationTheme

InputDecorationTheme(
  border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
  filled: true,
  fillColor: Colors.white,
)
6

Dark Mode and Adaptive Theming

Supporting dark mode is essential for modern applications.

Dark Mode Setup

  • Provide both theme and darkTheme. Let users choose with themeMode: ThemeMode.system/User/Light/Dark.
  • Ensure contrast and iconography are legible in both modes; test color combinations and images.

Checklist for Dark Mode Readiness

  • Test on large textScaleFactor values.
  • Use semantic surface colors rather than fixed backgrounds.
  • Prefer dynamic image variants or apply color filters to icons where appropriate.
7

Creating Reusable Custom Widgets

Building custom widgets improves code reusability and maintainability.

Best Practices

  • Build custom widgets by composing existing widgets; prefer StatelessWidget for presentational widgets and StatefulWidget only when local state is necessary.
  • Make widgets configurable via constructor parameters (text, colors, callbacks) and provide sensible defaults.

Pattern: Small, Focused Widget

class ProfileCard extends StatelessWidget {
  final String name;
  final String role;
  final VoidCallback? onFollow;

  const ProfileCard({
    Key? key,
    required this.name,
    required this.role,
    this.onFollow,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Row(
          children: [
            CircleAvatar(child: Icon(Icons.person)),
            SizedBox(width: 12),
            Expanded(child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(name, style: Theme.of(context).textTheme.subtitle1),
                Text(role, style: Theme.of(context).textTheme.bodyText2),
              ],
            )),
            ElevatedButton(onPressed: onFollow, child: Text('Follow')),
          ],
        ),
      ),
    );
  }
}

Design note: Expose callbacks for interactivity and avoid tightly coupling widget to application services.

8

Theming Custom Widgets

Custom widgets should integrate seamlessly with your app's theme.

Theme Integration

Custom widgets should consume theme tokens (colors, text styles) rather than defining their own hard-coded styles. This keeps custom widgets consistent with the app theme.

Example

final titleStyle = Theme.of(context).textTheme.subtitle1?.copyWith(
  color: Theme.of(context).colorScheme.primary
);
9

Config and Style Propagation: InheritedWidget and Provider

Understanding how to propagate configuration and styles down the widget tree is essential for scalable apps.

When to Use Each

  • Use InheritedWidget for lightweight, performance-friendly propagation of config down the tree. For app-wide state or theme toggles, prefer Provider for ergonomics and testability.
  • Example use cases: current locale, responsive breakpoints, feature flags, or custom theme overrides.

Conceptual Pattern

class AppConfig extends InheritedWidget {
  final String apiBaseUrl;
  const AppConfig({required this.apiBaseUrl, required Widget child}) : super(child: child);

  static AppConfig of(BuildContext context) => context.dependOnInheritedWidgetOfExactType()!;
  @override bool updateShouldNotify(covariant AppConfig old) => apiBaseUrl != old.apiBaseUrl;
}

Guideline: Don't overuse; prefer Provider or other state management solutions for complex scenarios.

10

Creating Adaptive Widgets

Adaptive widgets provide platform-appropriate experiences.

Platform Adaptation

Make widgets that adapt to platform or size: use Platform.isIOS checks sparingly, prefer Theme.of(context).platform or conditional widgets that use Material or Cupertino components as required.

Pattern: Adaptive Button

Widget adaptiveButton(BuildContext context, String label, VoidCallback onPressed) {
  if (Theme.of(context).platform == TargetPlatform.iOS) {
    return CupertinoButton(child: Text(label), onPressed: onPressed);
  }
  return ElevatedButton(onPressed: onPressed, child: Text(label));
}
11

Accessibility Considerations in Styling

Accessible styling ensures your app works for all users.

Accessibility Best Practices

  • Ensure contrast ratio between text and background meets accessibility standards.
  • Respect large font sizes by using relative sizing via TextTheme and MediaQuery.textScaleFactor.
  • Provide semantic labels for interactive custom widgets and ensure tap targets meet minimum size.
12

Theming Strategy and File Organization

Organizing theme files properly makes maintenance easier as your app grows.

Suggested File Layout

  • lib/theme/app_theme.dart — central ThemeData builder and toggles
  • lib/theme/app_colors.dart — color tokens
  • lib/theme/typography.dart — text styles and font families
  • lib/widgets/ — reusable custom widgets (ProfileCard, PrimaryButton)
  • lib/config/ — AppConfig or constants

Reason: Separation of concerns keeps style maintenance scalable as the app grows.

13

Exercises

Practice what you've learned with these exercises:

1. App theme implementation

Create AppTheme file with light and dark ThemeData. Use a seed color and customize textTheme, elevatedButtonTheme, and inputDecorationTheme. Provide a toggle that switches theme at runtime using Provider or ValueNotifier.

2. Custom PrimaryButton widget

Implement PrimaryButton that reads colors from Theme and accepts isLoading to show a CircularProgressIndicator. Make it accessible with semantic labels.

3. Themed profile card

Reuse ProfileCard from earlier, but refactor it to use theme tokens for paddings, colors, and text styles. Add a variant parameter (compact, regular) to change spacing and font sizes.

4. Dark mode audit

Build a small screen with mixed content (images, text, icons) and switch between light/dark themes. Document any issues found (contrast, unreadable icons) and fix them.

14

Session Assignment

Complete Exercises 1–3. Provide screenshots for light and dark themes, and include a short README describing theme tokens used, why they were chosen, and how to extend the theme for new components.